##################################################################
# File : /lua/aibrain.lua
# Description : AI Brain
# Author : GPG
##################################################################

local Utils = import('/lua/utilities.lua')

AIBrain = Class(moho.aibrain_methods) {
    Trash = TrashBag(),

    ForkThread = function(self, fn, ...)
        if fn then
            local thread = ForkThread(fn, self, unpack(arg))
            self.Trash:Add(thread)
            return thread
        else
            return nil
        end
    end,

#     ClassCallbacks = {
#         OnGiveResource = Callback(),
#         OnVictory = Callback(),
#         OnGiveScore = Callback(),
#     },

#     #####################################################################
#     ## ------------- HUMAN BRAIN FUNCTIONS HANDLED HERE  ------------- ##
#     #####################################################################
#     OnCreateHuman = function(self, planName)
#         self:InitScore()
#         self:OnCreateAny(planName)
#         self.BrainController = 'Human'
#         if ScenarioInfo.Options.FullAI and not ScenarioInfo.Options.NoAI then
#             self.BrainController = 'AI'
#             self.GoalPlanner:SetActive(true)
#         end
#     end,

#     OnCreateAI = function(self, planName)
#         self:InitScore()
#         self:OnCreateAny(planName)
#         if not ScenarioInfo.Options.NoAI and not string.find( self.Name, 'TEAM_' ) then
#             self.GoalPlanner:SetActive(true)
#         end
#         self.BrainController = 'AI'
#     end,

#     OnCreateAny = function(self, planName)
#         self.Callbacks = {
#             OnIntelChange = Callback(),
#             UnitAdded = Callback(),
#             Ping = Callback(),
#         }
#         # Setup data containers for the AI
#         self.AIData = {}
#         self.GoalPlanner = BrainPlanner(self)
#         local teamBrain = false
#         if string.find( self.Name, 'TEAM_' ) and not ScenarioInfo.Options.NoInitialUnits then
#             teamBrain = true
#             self.TeamBrain = true
#             self:InitGameStateInfo()
#         end

#         if teamBrain and not ScenarioInfo.Options.NoAI then
#             self:ForkThread( self.CreateStrategicGOAP )
#         end

#         if teamBrain then
#             self.DescriptiveName = 'Team Brain ' .. self:GetArmyIndex()
#         else
#             self.DescriptiveName = 'Army Brain ' .. self:GetArmyIndex()
#             if not ArmyIsCivilian(self:GetArmyIndex()) then
#                 self.Achievements = ScenarioInfo.ArmySetup[self.Name].Achievements or {}
#                 #LOG("*DEBUG: Initial Brain "..self.Name.." Achievements for Brain: ", repr(self.Achievements))
#             end
#         end

#         self.AbilityData = {}
#         self.ArmyBonusData = {}
#         InitializePathGraphs(self)
#         self.MultiKillStreak = 0
#     end,

#     CreateStrategicGOAP = function(self)
#         self.GOAP = StrategicGOAP(self)
#     end,

#     AddFriendlyAsset = function(self, asset)
#         local hero = self:GetHero()
#         if hero and not hero:IsDead() and hero.GOAP then
#             hero.GOAP:AddFriendlyAsset(asset)
#         end
#     end,

#     AbandonedByPlayer = function(self)
#         self.BrainController = 'AI'
#         self.GoalPlanner:SetActive(true)
#         self:GetTeamArmy().GOAP:AddBrain(self)
#         local hero = self:GetHero()
#         if hero and not hero:IsDead() then
#             self.GoalPlanner:StartHeroPlan(hero)
#         end
#     end,

#     OnSpawnPreBuiltUnits = function(self)
#     end,

#     InitializePlatoonBuildManager = function(self)
#     end,


#     InitGameStateInfo = function(self)
#         self.GameState = {
#             Flags = {},
#             Portals = {},
#             Strongholds = {},
#         }
#         self:ForkThread(self.GameStateThread)
#     end,

#     #This game state thread does more work than it needs to.  Make this better later.
#     GameStateThread = function(self)
#         local enemyTeamBrain
#         local neutCivBrain
#         local allyTeamBrain
#         while true do

#             for k, brain in ArmyBrains do
#                 if brain.TeamBrain and IsEnemy(self:GetArmyIndex(), brain:GetArmyIndex()) then
#                     enemyTeamBrain = brain
#                 elseif brain.TeamBrain and IsAlly(self:GetArmyIndex(), brain:GetArmyIndex()) then
#                     allyTeamBrain = brain
#                 elseif brain.Name == 'NEUTRAL_CIVILIAN' then
#                     neutCivBrain = brain
#                 end
#             end

#             self:CheckFlags(enemyTeamBrain, neutCivBrain)
#             self:CheckPortals()
#             self:CheckStrongholds()
#             if not Sync.GameState then
#                 Sync.GameState = {}
#             end
#             for k, brain in ArmyBrains do
#                 local army = brain:GetArmyIndex()
#                 if IsAlly(army, allyTeamBrain:GetArmyIndex()) then
#                     Sync.GameState[army] = table.copy(self.GameState)
#                 end
#             end
#             WaitSeconds(0.3)
#         end
#     end,

#     CheckFlags = function(self, etb, ncb)
#         local eflags
#         local nflags
#         if etb then
#             eflags = etb:GetListOfUnits(categories.FLAG, false)
#         end
#         if ncb then
#             nflags = ncb:GetListOfUnits(categories.FLAG, false)
#         end
#         local aflags = self:GetListOfUnits(categories.FLAG, false)
#         local allflags = Utils.TableCat(eflags, nflags, aflags)
#         for k, flag in allflags do
#             local flgtbl = {
#                 Name = flag.UnitName,
#                 OwnPct = flag.OwnedPct,
#                 OwnedBy = flag:GetAIBrain().Name,

#                 EntityId = flag:GetEntityId(),
#             }
#             local teamname = 'Not Owned'
#             if flag:GetAIBrain().Conquest then
#                 flgtbl.OwnerName = flag:GetAIBrain().Conquest.TeamName or 'Not Owned'
#             end
#             local blip = flag:GetBlip(flag:GetArmy())
#             if blip then
#                 flgtbl.BlipId = blip:GetEntityId()
#             end
#             self.GameState.Flags[flag.UnitName] = flgtbl
#         end
#     end,

#     CheckPortals = function(self)
#         local portals = self:GetListOfUnits(categories.PORTAL, false)
#         for k, port in portals do
#             local prttbl = {
#                 Name = port.UnitName,
#                 EntityId = port:GetEntityId(),
#                 HealthPct = port:GetHealth() / port:GetMaxHealth(),
#                 HealthCurrent = math.ceil(port:GetHealth()),
#                 HealthMax = math.ceil(port:GetMaxHealth()),
#             }
#             self.GameState.Portals[port.UnitName] = prttbl
#         end
#     end,

#     CheckStrongholds = function(self)
#         local strongholds = self:GetListOfUnits(categories.STRONGHOLD, false)
#         for k, stghld in strongholds do
#             if table.getsize(self.GameState.Strongholds) == 0 or table.find(table.keys(self.GameState.Strongholds), stghld.UnitName) then
#                 local stghldtbl = {
#                     Name = stghld.UnitName,
#                     EntityId = stghld:GetEntityId(),
#                     HealthPct = stghld:GetHealth() / stghld:GetMaxHealth(),
#                     HealthCurrent = math.ceil(stghld:GetHealth()),
#                     HealthMax = math.ceil(stghld:GetMaxHealth()),
#                 }
#                 self.GameState.Strongholds[stghld.UnitName] = stghldtbl
#             end
#         end
#     end,

#     InitScore = function(self)
#         self.Score = {
#             BonusPoints = 0,
#             Damage = 0,
#             Gold = 0,
#             Grunts = 0,
#             HeroAssists = 0,
#             HeroFlags = 0,
#             HeroDeaths = 0,
#             HeroId = ScenarioInfo.ArmySetup[self.Name].Hero,
#             HeroKills = 0,
#             HeroKillStreak = 0,
#             HeroLevel = 1,
#             HeroPing = 0,
#             HeroScore = 0,
#             HeroSimSpeed = 0,
#             Special = 0,
#             Structures = 0,
#             UnitCurrent = 0,
#             UnitCap = 0,
#             WarRank = 0,
#             LongestKillStreak = 0,
#         }

#         if not ScoreThread then
#             ScoreThread = ForkThread(
#                 function()
#                     while true do
#                         Sync.Score = {}
#                         for k,v in ArmyBrains do
#                             v.Score.UnitCurrent = v:GetArmyStat("UnitCap_Current", 0.0).Value
#                             v.Score.UnitCap = v:GetArmyStat("UnitCap_MaxCap", 0.0).Value

#                             v.Score.Special = v:GetCurrentUnits( ParseEntityCategory("SPECIALUNIT") )

#                             Sync.Score[v:GetArmyIndex()] = table.copy(v.Score)
#                         end
#                         WaitSeconds(1)
#                     end
#                 end
#             )
#         end
#     end,

#     AddHeroKillStreak = function(self)
#         if not self.TeamBrain then

#             self:AnnounceFirstKill()
#             self:AnnounceMultiKill()

#             local streak = self.Score.HeroKillStreak + 1
#             self.Score.HeroKillStreak = streak
#             if streak > self.Score.LongestKillStreak then
#                 self.Score.LongestKillStreak = streak
#             end
#             if streak > table.getsize(GameData.Announcements.KillStreak) then
#                 streak = table.getsize(GameData.Announcements.KillStreak)
#             end
#             local announcement = GameData.Announcements.KillStreak[streak]
#             if announcement then
#                 if announcement.Text then
#                     local short = self.Nickname .. ' is '
#                     Announce(short, announcement.Text, nil, 3, announcement.Audio)
#                 end
#             end
#         end
#     end,

#     ResetHeroKillStreak = function(self)
#         self.Score.HeroKillStreak = 0
#     end,

#     AnnounceFirstKill = function(self)
#         if not self.FirstKillDone then
#             local first = true
#             for k, brain in ArmyBrains do
#                 if (brain != self and brain.FirstKillDone) then
#                     self.FirstKillDone = true
#                     first = false
#                     break
#                 end
#             end
#             if first then
#                 self.FirstKillDone = true
#                 local announcement = GameData.Announcements.FirstKill
#                 if announcement then
#                     if announcement.Text then
#                         Announce(self.Nickname, announcement.Text, nil, 2, announcement.Audio)
#                     end
#                 end
#             end
#         end
#     end,

#     AnnounceMultiKill = function(self)
#         self.MultiKillStreak = self.MultiKillStreak + 1
#         if self.MultiKilling and self.MultiKillStreak > 1 then
#             local streak = self.MultiKillStreak
#             if streak > table.getsize(GameData.Announcements.MultiKill) then
#                 streak = table.getsize(GameData.Announcements.MultiKill)
#             end
#             local announcement = GameData.Announcements.MultiKill[streak]
#             if announcement then
#                 if announcement.Text then
#                     local short = self.Nickname .. ':'
#                     Announce(short, announcement.Text, nil, 2, announcement.Audio)
#                 end
#             end

#         end
#         self.MultiKilling = true
#         if self.MKT then
#             self.MKT:Destroy()
#             self.MKT = nil
#         end
#         self.MKT = self:ForkThread(self.MultiKillThread)
#     end,

#     MultiKillThread = function(self)
#         WaitSeconds(10)
#         self.MultiKillStreak = 0
#         self.MultiKilling = false
#     end,


#     GiveScore = function(self, scoreType, level)
#         local score = GameData.Scoring[scoreType] or 0
#         if level then
#             score = score * level
#         end
#         self.Score.HeroScore = self.Score.HeroScore + score
#         AIBrain.ClassCallbacks.OnGiveScore:Call(self, scoreType, score)
#     end,

#     # Returns true if not at the unit cap
#     CheckUnitCap = function(self, unitCost)
#         local total = GetArmyUnitCostTotal(self:GetArmyIndex())
#         local cap = GetArmyUnitCap(self:GetArmyIndex())

#         unitCost = unitCost or 1

#         # Add 1 because if the cap isn't at an integer it will overflow; ie: 99.4 will overflow on the purchase of a single soldier
#         if (total + unitCost) > cap then
#             return false
#         end

#         return true
#     end,

#     UpdateGameResult = function(self, index, result)
#         if(not self.Conquest.IsTeamArmy and not self.Conquest.IsNeutralArmy) then
#             if(not Sync.GameResult) then Sync.GameResult = {} end
#             Sync.GameResult[index] = {
#                 BonusPoints = self.Score.BonusPoints,
#                 Damage = self.Score.Damage,
#                 Gold = self.Score.Gold,
#                 Grunts = self.Score.Grunts,
#                 HeroAssists = self.Score.HeroAssists,
#                 HeroDeaths = self.Score.HeroDeaths,
#                 HeroFlags = self.Score.HeroFlags,
#                 HeroId = ScenarioInfo.ArmySetup[self.Name].Hero,
#                 HeroKills = self.Score.HeroKills,
#                 HeroKillStreak = self.Score.HeroKillStreak,
#                 HeroLevel = self.Score.HeroLevel,
#                 Options = ScenarioInfo.Options,
#                 PlayerNick = self.Nickname,
#                 Result = result,
#                 Score = self.Score.HeroScore,
#                 Structures = self.Score.Structures,
#             }
#         end
#     end,

#     # I'm defeated, show the defeat screen.
#     OnDefeat = function(self)
#         local index = self:GetArmyIndex()
#         SetArmyOutOfGame(index)
#         self:UpdateGameResult(index, 'defeat')

#         import('/lua/SimUtils.lua').UpdateUnitCap()
#         local function KillArmy()
#             WaitSeconds(20)
#             local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false)
#             for k, unit in units do
#                 unit:KillSelf()
#             end
#         end
#         ForkThread(KillArmy)
#     end,

#     OnVictory = function(self)
#         LOG('OnVictory!')

#         local index = self:GetArmyIndex()
#         self:UpdateGameResult(index, 'victory')

#         AIBrain.ClassCallbacks.OnVictory:Call(self)
#     end,

#     OnDraw = function(self)
#         if not Sync.GameResult then Sync.GameResult = {} end
#         table.insert(Sync.GameResult, { self:GetArmyIndex(), "draw", ScenarioInfo.Options })
#     end,

#     ScalableIncome = {
#         Bounty = true,
#         MoneyProduction = true,
#         FlagCapture = true,
#     },

#     GiveResource = function(self,type,amount,origin)
#         if self.ScalableIncome[origin] then
#             local multiplier = Game.GameData.GoldIncomeMultiplier[ ScenarioInfo.Options.GoldIncome ] or 1
#             local handicap = Game.GameData.GameHandicap[ self.Name .. 'GoldMultiplier' ] or 1
#             amount = amount * multiplier * handicap
#         end

#         self:InternalGiveResource(type,amount)
#         AIBrain.ClassCallbacks.OnGiveResource:Call(self,type,amount,origin or 'Unknown')
#     end,

#     IsDefeated = function(self)
#         return ArmyIsOutOfGame(self:GetArmyIndex())
#     end,

#     OnIntelChange = function(self,blip,intel,val)
#         self.Callbacks.OnIntelChange:Call(self, blip, intel, val)
#         local focus = GetFocusArmy()
#         if self:GetArmyIndex() == focus then
#             local source = blip:GetSource()
#             if source and blip:IsSeenEver() and source.Sync.BlipId != blip:GetEntityId() then
#                 Sync.BlipMap[blip:GetEntityId()] = source:GetEntityId()
#             end
#         end
#     end,

#     # Army Bonuses are global buffs and abilities applied to all units of an army
#     # They are added by items/flags/etc.  This system manages them on a specific army
#     AddArmyBonus = function(self, bonusName, unit)
#         local def = ArmyBonuses[bonusName]

#         # Make sure we have the bonus for this
#         if not def then
#             WARN('Could not find ArmyBonus - ' .. bonusName)
#             return
#         end

#         # Find which armies to apply the bonus to; We default to just this army
#         # If the blueprint calls for the whole Team then we find all the team armies
#         local armies = { self }
#         if def.ApplyArmies == 'Team' then
#             armies = self:GetAlliedArmies()
#         end

#         # Apply the army Bonus to each of the valid armies
#         for _,brain in armies do
#             # Add the data to the brain so we can apply to any new units later
#             table.insert(brain.ArmyBonusData, bonusName)

#             # Apply the new bonus to each of the units in the army that deserves the bonus
#             local units = brain:GetListOfUnits( ParseEntityCategoryEx( def.UnitCategory ), false )
#             for k,unit in units do
#                 if unit:IsDead() then
#                     continue
#                 end

#                 self:ApplyArmyBonusToUnit( unit, bonusName )
#             end
#         end

#         # If the army bonus is to be removed on the unit's death/capture, we set up the callbacks
#         # for this here
#         if def.RemoveOnUnitDeath then
#             local function RemoveOnDeathCallback(bonusName, unit)
#                 unit:GetAIBrain():RemoveArmyBonus(bonusName)
#             end
#             unit.Callbacks.OnKilled:Add( RemoveOnDeathCallback, bonusName )
#             unit.Callbacks.OnTransfered:Add( RemoveOnDeathCallback, bonusName )
#         end
#     end,

#     RemoveArmyBonus = function(self, bonusName)
#         local def = ArmyBonuses[bonusName]

#         # Find which armies to remove the bonus from; We default to just this army
#         # If the blueprint calls for the whole Team then we find all the team armies
#         local armies = { self }
#         if def.ApplyArmies == 'Team' then
#             armies = self:GetAlliedArmies()
#         end

#         # Remove the army Bonus from each of the valid armies
#         for _,brain in armies do
#             # Find the first instance of the bonus name in our storage and pull from the table
#             for k,v in brain.ArmyBonusData do
#                 if v == bonusName then
#                     table.remove( brain.ArmyBonusData, k )
#                 end
#             end

#             # Remove the new bonus to each of the units in the army that had the bonus
#             local units = brain:GetListOfUnits( ParseEntityCategoryEx( def.UnitCategory ), false )
#             for k,unit in units do
#                 if unit:IsDead() then
#                     continue
#                 end

#                 self:RemoveArmyBonusFromUnit( unit, bonusName )
#             end
#         end
#     end,

#     # Applies the new abilities and buffs from the ArmyBonus to given unit
#     ApplyArmyBonusToUnit = function(self, unit, bonusName)
#         local def = ArmyBonuses[bonusName]

#         if not EntityCategoryContains( ParseEntityCategoryEx( def.UnitCategory ), unit ) then
#             return
#         end

#         if def.Buffs then
#             for _,buffName in def.Buffs do
#                 Buff.ApplyBuff(unit, buffName)
#             end
#         end

#         if def.Abilities then
#             for _,abilityName in def.Abilities do
#                 Ability.AddAbility(unit, abilityName)
#             end
#         end
#     end,

#     # Removes the abilities and buffs from the ArmyBonus on given unit
#     RemoveArmyBonusFromUnit = function(self, unit, bonusName)
#         local def = ArmyBonuses[bonusName]

#         if not EntityCategoryContains( ParseEntityCategoryEx( def.UnitCategory ), unit ) then
#             return
#         end

#         if def.Buffs then
#             for _,buffName in def.Buffs do
#                 Buff.RemoveBuff(unit, buffName)
#             end
#         end

#         if def.Abilities then
#             for _,abilityName in def.Abilities do
#                 Ability.RemoveAbility(unit, abilityName)
#             end
#         end
#     end,

#     UnitAddedToArmy = function(self, unit)
#         if EntityCategoryContains( categories.HERO, unit ) then
#             self.Hero = unit
#         end

#         # Update the unit with any army bonuses that we have added
#         for k,v in self.ArmyBonusData do
#             self:ApplyArmyBonusToUnit(unit, v)
#         end

#         self.Callbacks.UnitAdded:Call(self,unit)
#     end,

#     PortalPlatoonCreated = function(self, platoon)
#         if platoon.LaneData then
#             if self.GOAP then
#                 self.GOAP:AddPlatoonToLane(platoon, platoon.LaneData.LaneName)
#             end
#         end
#     end,

#     GetHero = function(self)
#         if not self.Hero or self.Hero:IsDead() then
#             return false
#         end
#         return self.Hero
#     end,

#     GetHealthStatue = function(self)
#         if not self.TeamBrain then
#             local teamArmy = self:GetTeamArmy()
#             if not teamArmy then
#                 return false
#             end
#             return teamArmy:GetHealthStatue()
#         end

#         if self.HealthStatue and not self.HealthStatue:IsDead() then
#             return self.HealthStatue
#         end

#         local statues = self:GetListOfUnits( categories.HEALTHSTATUE, false, false )
#         if statues[1] and not statues[1]:IsDead() then
#             self.HealthStatue = statues[1]
#             return self.HealthStatue
#         end

#         self.HealthStatue = false
#         return false
#     end,

#     GetStronghold = function(self)
#         if not self.TeamBrain then
#             return self:GetTeamArmy():GetStronghold()
#         end

#         if self.Stronghold and not self.Stronghold:IsDead() then
#             return self.Stronghold
#         end

#         local strongholds = self:GetListOfUnits( categories.CONQUESTOBJECTIVE, false, false )
#         if strongholds[1] and not strongholds[1]:IsDead() then
#             self.Stronghold = strongholds[1]
#             return self.Stronghold
#         end

#         self.Stronghold = false
#         return false
#     end,

#     GetTeamArmy = function(self)
#         if self.TeamArmy then
#             return self.TeamArmy
#         end

#         for k,v in ArmyBrains do
#             if not v.TeamBrain then
#                 continue
#             end

#             if not IsAlly( self:GetArmyIndex(), v:GetArmyIndex() ) then
#                 continue
#             end

#             self.TeamArmy = v
#             return v
#         end

#         return false
#     end,

#     # Returns a table with all the allied armies of the current brain
#     GetAlliedArmies = function(self)
#         local armies = {}

#         local aIndex = self:GetArmyIndex()
#         for _,brain in ArmyBrains do
#             local bIndex = brain:GetArmyIndex()
#             if aIndex != bIndex and not IsAlly( aIndex, bIndex ) then
#                 continue
#             end

#             table.insert( armies, brain )
#         end

#         return armies
#     end,

#     AddTowerIdCache = function(self)
#         self.TowerIds = {}
#     end,

#     AddTowerId = function(self, id, unit)
#         self.TowerIds[id] = unit
#     end,

#     GetTowerGraphId = function(self)
#         if self.TowerId then
#             return self.TowerId
#         end

#         local team = self:GetTeamArmy()
#         if not team then
#             team = self
#         end

#         if team.TowerId then
#             self.TowerId = team.TowerId
#             return self.TowerId
#         end

#         teamIndex = team:GetArmyIndex()
#         AddTowerGraph(teamIndex)
#         team:AddTowerIdCache()

#         team.TowerId = teamIndex
#         self.TowerId = teamIndex
#         return self.TowerId
#     end,

#     OnBuildRestrictionChange = function(self, old, new)
#         Sync.BuildRestrictionChange = true
#     end,

#     Alert = function(self, unit, key)
#         if(key and unit and not unit:IsDead()) then
#             if(key == 'Defenses' or key == 'OuterDefenses') then
#                 local hero = self:GetUnitsAroundPoint(categories.HERO, unit:GetPosition(), 10, 'Enemy')
#                 local creeps = self:GetUnitsAroundPoint(categories.MOBILE, unit:GetPosition(), 10, 'Enemy')
#                 if(table.getn(hero) > 0 or table.getn(creeps) > 4) then
#                     ForkThread(
#                         function()
#                             if(ScenarioInfo.GiveDefenseWarning[self.Conquest.TeamArmy]) then
#                                 if(self:SoundAlert(GameData.Alerts[key], unit)) then
#                                     ScenarioInfo.GiveDefenseWarning[self.Conquest.TeamArmy] = false
#                                     WaitSeconds(GameData.Alerts[key].VOResetTime or 30)
#                                     ScenarioInfo.GiveDefenseWarning[self.Conquest.TeamArmy] = true
#                                 end
#                             end
#                         end
#                     )
#                 end
#             elseif(key == 'Stronghold') then
#                 ForkThread(
#                     function()
#                         if(unit.CanWarn) then
#                             if(self:SoundAlert(GameData.Alerts[key], unit)) then
#                                 unit.CanWarn = false
#                                 WaitSeconds(GameData.Alerts[key].VOResetTime or 30)
#                                 if(unit and not unit:IsDead()) then
#                                     unit.CanWarn = true
#                                 end
#                             end
#                         end
#                     end
#                 )
#             elseif(key == 'Portal') then
#                 ForkThread(
#                     function()
#                         if(ScenarioInfo.GivePortalWarning[self.Conquest.TeamArmy]) then
#                             if(self:SoundAlert(GameData.Alerts[key], unit)) then
#                                 ScenarioInfo.GivePortalWarning[self.Conquest.TeamArmy] = false
#                                 WaitSeconds(GameData.Alerts[key].VOResetTime or 30)
#                                 ScenarioInfo.GivePortalWarning[self.Conquest.TeamArmy] = true
#                             end
#                         end
#                     end
#                 )
#             elseif(string.find(key, 'Reinforcement')) then
#                 if(ScenarioInfo.GiveReinforcementWarning[self.Conquest.TeamArmy][tonumber(string.sub(key, 14))]) then
#                     if(self:SoundAlert(GameData.Alerts[key], unit)) then
#                         ScenarioInfo.GiveReinforcementWarning[self.Conquest.TeamArmy][tonumber(string.sub(key, 14))] = false
#                     end
#                 end
#             else
#                 self:SoundAlert(GameData.Alerts[key], unit)
#             end
#         end
#     end,

#     SoundAlert = function(self, alert, unit)
#         local allySound = alert.AllySound
#         local enemySound = alert.EnemySound

#         if allySound or enemySound then
#             if not Sync.ArmySounds then Sync.ArmySounds = {} end
#             table.insert(Sync.ArmySounds, {Army = self:GetArmyIndex(), AllySound = allySound, EnemySound = enemySound})
#             self:DisplayAlert(alert, unit)
#             return true
#         else
#             return false
#         end
#     end,

#     DisplayAlert = function(self, alert, unit)
#         if(alert.Effects) then
#             if(unit and not unit:IsDead()) then
#                 ForkThread(
#                     function()
#                         local effects = CreateTemplatedEffectAtPos( alert.Effects.Base, nil, alert.Effects.Template, unit:GetArmy(), unit:GetPosition() )
#                         #Play Sound when Slider Used
#                         unit:PlaySound( 'Forge/CREEPS/Priest/snd_cr_priest_cast_heal_hit' )  #PLACEHOLDER
#                         WaitSeconds(alert.EffectsResetTime or 5)
#                         for k, v in effects do
#                             v:Destroy()
#                         end
#                     end
#                 )
#             end
#         end
#     end,

#     OnPing = function(self, ping, data)
#         if self.GOAP then
#             self.GOAP:AddPing(ping, data)
#         end

#         self.Callbacks.Ping:Call(ping, data)
#     end,

#     EnableStats = function(self)
#         self.StatsThread = self:ForkThread( self.StatsFunction )
#     end,

#     EnableAIStats = function(self)
#         self.AIStatsThread = self:ForkThread( self.AIStatsFunction )
#     end,

#     StatsFunction = function(self)
#         while true do
#             local hero = self:GetHero()
#             if not hero or hero:IsDead() then
#                 self:HeroDeadStats()
#                 WaitSeconds(1)
#                 continue
#             end

#             self:HeroDistanceStats(hero)

#             WaitSeconds(1)
#         end
#     end,

#     HeroDeadStats = function(self)
#         local team = self.Conquest.TeamArmy
#         local armyName = self.Name
#         local hero = self.Hero.A_ID
#         local nickName = self.Nickname
#         if(team and armyName and hero and nickName) then
#             if(armyName != 'TEAM_1' and armyName != 'TEAM_2') then
#                 AddLogEntry('HeroStatusSeconds', {Team = team, ArmyName = armyName, Hero = hero, NickName = nickName, HeroStatus = {HeroDead = true}})
#             end
#         end
#     end,

#     HeroDistanceStats = function(self, hero)
#         local aiBrain = hero:GetAIBrain()
#         local heroArmy = hero:GetArmy()

#         local units = {
#             StructuresMaxRange = aiBrain:GetUnitsAroundPoint( categories.STRUCTURE, hero.Position, 20 ),
#             StructuresMediumRange = aiBrain:GetUnitsAroundPoint( categories.STRUCTURE, hero.Position, 15 ),

#             MobileMaxRange = aiBrain:GetUnitsAroundPoint( categories.MOBILE, hero.Position, 20 ),
#             MobileMediumRange = aiBrain:GetUnitsAroundPoint( categories.MOBILE, hero.Position, 15 ),
#             MobileShortRange = aiBrain:GetUnitsAroundPoint( categories.MOBILE, hero.Position, 5 ),
#         }

#         local heroStatus = {
#             FriendlyStronghold = false,
#             FriendlyShop = false,
#             FriendlyFrontlineCreeps = false,
#             FriendlyRearlineCreeps = false,
#             FriendlyHero = false,
#             FriendlyDefense = false,
#             FriendlyFlag = false,

#             EnemyStronghold = false,
#             EnemyFrontlineCreeps = false,
#             EnemyRearlineCreeps = false,
#             EnemyHero = false,
#             EnemyDefense = false,
#             EnemyFlag = false,

#             NearEnemyNoFriendly = false,
#             NearFriendlyNoEnemy = false,
#             NearFriendlyWithEnemy = false,

#             NearEnemyDefenseNoFriendly = false,
#             NearEnemyDefenseWithFriendly = false,

#             NearFriendlyDefenseNoEnemy = false,
#             NearFriendlyDefenseWithEnemy = false,

#             NearFriendlyCastleNoEnemy = false,
#             NearFriendlyCastleWithEnemy = false,

#             NearFriendlyStrongholdNoEnemy = false,
#             NearFriendlyStrongholdWithEnemy = false,

#             NearFriendlyFlagNoEnemy = false,
#             NearFriendlyFlagWithEnemy = false,

#             NeutralFlag = false,

#             HeroDead = false,

#             # How often the units is frozen, casting, etc
#             Frozen = hero.IsFrozen,
#             RezSickness = false,
#             Casting = false,
#         }

#         if hero.CastingAbilityTask then
#             heroStatus.Casting = true
#         end

#         local rezSickness = {
#             'RezSickness01',
#             'RezSickness02',
#             'RezSickness03',
#             'RezSickness04',
#             'RezSickness05',
#             'RezSickness06',
#         }
#         for _,buffName in rezSickness do
#             if Buff.HasBuff(hero, buffName) then
#                 heroStatus.RezSickness = true
#                 break
#             end
#         end

#         # Check long range buildings - strongholds and castles are all that is counted for this
#         for k,v in units.StructuresMaxRange do
#             if EntityCategoryContains( categories.STRONGHOLD, v ) then
#                 if not heroStatus.FriendlyStronghold and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyStronghold = true
#                 elseif not heroStatus.EnemyStronghold and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyStronghold = true
#                 end
#             end
#         end

#         # Check for close range buildings - flags, defenses, and friendly shops
#         for k,v in units.StructuresMediumRange do
#             if EntityCategoryContains( categories.SHOP, v ) then
#                 if not heroStatus.FriendlyShop and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyShop = true
#                 end

#             elseif EntityCategoryContains( categories.DEFENSE, v ) then
#                 if not heroStatus.FriendlyDefense and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyDefense = true
#                 elseif not heroStatus.EnemyDefense and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyDefense = true
#                 end

#             elseif EntityCategoryContains( categories.FLAG, v ) then
#                 if not heroStatus.FriendlyFlag and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyFlag = true
#                 elseif not heroStatus.EnemyFlag and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyFlag = true
#                 elseif not heroStatus.NeutralFlag and IsNeutral( v:GetArmy(), heroArmy ) then
#                     heroStatus.NeutralFlag = true
#                 end
#             end
#         end

#         # Check for long range mobile units - Enemy Heroes
#         for k,v in units.MobileMaxRange do
#             if EntityCategoryContains( categories.HERO, v ) and v != hero then
#                 if not heroStatus.EnemyHero and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyHero = true
#                     break
#                 end
#             end
#         end

#         # Check for medium range mobile units - enemy archers and catapults
#         for k,v in units.MobileMediumRange do
#             if EntityCategoryContains( (categories.ARTILLERY + categories.HEALER + categories.ARCHER ) * categories.GRUNT, v ) then
#                 if not heroStatus.EnemyRearlineCreeps and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyRearlineCreeps = true
#                     break
#                 end
#             end
#         end

#         # Check for close range mobile - all friendly creeps and enemy infantry creeps
#         for k,v in units.MobileShortRange do
#             if not( heroStatus.FriendlyFrontlineCreeps or hero.EnemyFrontlineCreeps ) or EntityCategoryContains( categories.FRONTLINEINFANTRY + categories.GIANT, v ) then
#                 if not heroStatus.FriendlyFrontlineCreeps and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyFrontlineCreeps = true
#                 elseif not heroStatus.EnemyFrontlineCreeps and IsEnemy( v:GetArmy(), heroArmy ) then
#                     heroStatus.EnemyFrontlineCreeps = true
#                 end
#             end

#             if not( heroStatus.FriendlyRearlineCreeps ) or EntityCategoryContains( (categories.ARTILLERY + categories.HEALER + categories.ARCHER ) * categories.GRUNT, v ) then
#                 if not heroStatus.FriendlyFrontlineCreeps and IsAlly( v:GetArmy(), heroArmy ) then
#                     heroStatus.FriendlyRearlineCreeps = true
#                 end
#             end

#             if (heroStatus.FriendlyFrontlineCreeps and heroStatus.EnemyFrontlineCreeps and heroStatus.FriendlyRearlineCreeps) then
#                 break
#             end
#         end

#         if ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps ) and not ( heroStatus.FriendlyDefense or heroStatus.FriendlyFrontlineCreeps or heroStatus.FriendlyRearlineCreeps ) then
#             heroStatus.NearEnemyNoFriendly = true
#         end
#         if not ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps ) and ( heroStatus.FriendlyDefense or heroStatus.FriendlyFrontlineCreeps or heroStatus.FriendlyRearlineCreeps ) then
#             heroStatus.NearFriendlyNoEnemy = true
#         end
#         if ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps ) and ( heroStatus.FriendlyDefense or heroStatus.FriendlyFrontlineCreeps or heroStatus.FriendlyRearlineCreeps ) then
#             heroStatus.NearFriendlyWithEnemy = true
#         end

#         if ( heroStatus.EnemyDefense ) and not ( heroStatus.FriendlyDefense or heroStatus.FriendlyFrontlineCreeps or heroStatus.FriendlyRearlineCreeps ) then
#             heroStatus.NearEnemyDefenseNoFriendly = true
#         end
#         if ( heroStatus.EnemyDefense ) and ( heroStatus.FriendlyDefense or heroStatus.FriendlyFrontlineCreeps or heroStatus.FriendlyRearlineCreeps ) then
#             heroStatus.NearEnemyDefenseWithFriendly = true
#         end

#         if ( heroStatus.FriendlyDefense ) and not ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyDefenseNoEnemy = true
#         end
#         if ( heroStatus.FriendlyDefense ) and ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyDefenseWithEnemy = true
#         end

#         if ( heroStatus.FriendlyCastle ) and not ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyCastleNoEnemy = true
#         end
#         if ( heroStatus.FriendlyCastle ) and ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyCastleWithEnemy = true
#         end

#         if ( heroStatus.FriendlyStronghold ) and not ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyStrongholdNoEnemy = true
#         end
#         if ( heroStatus.FriendlyStronghold ) and ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyStrongholdWithEnemy = true
#         end

#         if ( heroStatus.FriendlyFlag ) and not ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyFlagNoEnemy = true
#         end
#         if ( heroStatus.FriendlyFlag ) and ( heroStatus.EnemyFrontlineCreeps or heroStatus.EnemyRearlineCreeps or heroStatus.EnemyHero ) then
#             heroStatus.NearFriendlyFlagWithEnemy = true
#         end

#         local team = self.Conquest.TeamArmy
#         local armyName = self.Name
#         local hero = self.Hero.A_ID
#         local nickName = self.Nickname
#         if(team and armyName and hero and nickName) then
#             if(armyName != 'TEAM_1' and armyName != 'TEAM_2') then
#                 AddLogEntry('HeroStatusSeconds', {Team = team, ArmyName = armyName, Hero = hero, NickName = nickName, HeroStatus = heroStatus})
#             end
#         end
#     end,

#     AIStatsFunction = function(self)
#         while true do
#             local hero = self:GetHero()

#             if self.BrainController == 'AI' then
#                 self:HeroAIStats(hero)
#             end

#             WaitSeconds(1)
#         end
#     end,

#     HeroAIStats = function(self, hero)
#         local team = self.Conquest.TeamArmy
#         if(team) then
#             if not hero or hero:IsDead() then
#                 AddLogEntry('HeroAIStatusSeconds',
#                     {
#                         Team = team,
#                         ArmyName = self.Name,
#                         Hero = self.Score.HeroId,
#                         NickName = self.Nickname,
#                         HeroStatus = {
#                             HeroDead = true,
#                         },
#                     }
#                 )
#                 return
#             end

#             local aiBrain = hero:GetAIBrain()
#             local heroArmy = hero:GetArmy()

#             local heroStatus = {
#             }

#             local currentMaster = hero.GOAP.CurrentMasterGoal
#             if currentMaster then
#                 heroStatus[currentMaster] = true
#             end

#             AddLogEntry('HeroAIStatusSeconds',
#                 {
#                     Team = team,
#                     ArmyName = self.Name,
#                     Hero = self.Score.HeroId,
#                     NickName = self.Nickname,
#                     HeroStatus = heroStatus,
#                 }
#             )

#             local squadStatus = {}
#             local squadState = hero.GOAP.SquadState
#             if squadState then
#                 squadStatus[squadState] = true
#             end

#             AddLogEntry('SquadAIStatusSeconds',
#                 {
#                     Team = team,
#                     ArmyName = self.Name,
#                     Hero = self.Score.HeroId,
#                     NickName = self.Nickname,
#                     SquadStatus = squadStatus,
#                 }
#             )
#         end
#     end,
}
